/*
 * Driver O/S-independent utility routines
 *
 * Copyright (C) 1999-2019, Broadcom.
 *
 *      Unless you and Broadcom execute a separate written software license
 * agreement governing use of this software, this software is licensed to you
 * under the terms of the GNU General Public License version 2 (the "GPL"),
 * available at http://www.broadcom.com/licenses/GPLv2.php, with the
 * following added to such license:
 *
 *      As a special exception, the copyright holders of this software give you
 * permission to link this software with independent modules, and to copy and
 * distribute the resulting executable under terms of your choice, provided that
 * you also meet, for each linked independent module, the terms and conditions
 * of the license of that module.  An independent module is a module which is
 * not derived from this software.  The special exception does not apply to any
 * modifications of the software.
 *
 *      Notwithstanding the above, under no circumstances may you combine this
 * software in any way with any other Broadcom software provided under a license
 * other than the GPL, without Broadcom's express prior written consent.
 *
 *
 * <<Broadcom-WL-IPTag/Open:>>
 *
 * $Id: bcmxtlv.c 788740 2018-11-13 21:45:01Z $
 */

#include <bcm_cfg.h>

#include <typedefs.h>
#include <bcmdefs.h>

#include <stdarg.h>

#ifdef BCMDRIVER
#include <osl.h>
#else /* !BCMDRIVER */
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#ifndef ASSERT
#define ASSERT(exp)
#endif // endif
#endif /* !BCMDRIVER */

#include <bcmtlv.h>
#include <bcmendian.h>
#include <bcmutils.h>

int bcm_xtlv_hdr_size(bcm_xtlv_opts_t opts)
{
    int len = (int)OFFSETOF(bcm_xtlv_t, data); /* nominal */
    if (opts & BCM_XTLV_OPTION_LENU8) {
        --len;
    }
    if (opts & BCM_XTLV_OPTION_IDU8) {
        --len;
    }

    return len;
}

bool bcm_valid_xtlv(const bcm_xtlv_t *elt, int buf_len, bcm_xtlv_opts_t opts)
{
    return elt != NULL && buf_len >= bcm_xtlv_hdr_size(opts) &&
           buf_len >= bcm_xtlv_size(elt, opts);
}

int bcm_xtlv_size_for_data(int dlen, bcm_xtlv_opts_t opts)
{
    int hsz;

    hsz = bcm_xtlv_hdr_size(opts);
    return ((opts & BCM_XTLV_OPTION_ALIGN32) ? ALIGN_SIZE(dlen + hsz, 0x4)
                                             : (dlen + hsz));
}

int bcm_xtlv_size(const bcm_xtlv_t *elt, bcm_xtlv_opts_t opts)
{
    int size; /* size including header, data, and any pad */
    int len;  /* length wthout padding */

    len = BCM_XTLV_LEN_EX(elt, opts);
    size = bcm_xtlv_size_for_data(len, opts);
    return size;
}

int bcm_xtlv_len(const bcm_xtlv_t *elt, bcm_xtlv_opts_t opts)
{
    const uint8 *lenp;
    int len;

    lenp = (const uint8 *)&elt->len; /* nominal */
    if (opts & BCM_XTLV_OPTION_IDU8) {
        --lenp;
    }

    if (opts & BCM_XTLV_OPTION_LENU8) {
        len = *lenp;
    } else if (opts & BCM_XTLV_OPTION_LENBE) {
        len = (uint32)hton16(elt->len);
    } else {
        len = ltoh16_ua(lenp);
    }

    return len;
}

int bcm_xtlv_id(const bcm_xtlv_t *elt, bcm_xtlv_opts_t opts)
{
    int id = 0;
    if (opts & BCM_XTLV_OPTION_IDU8) {
        id = *(const uint8 *)elt;
    } else if (opts & BCM_XTLV_OPTION_IDBE) {
        id = (uint32)hton16(elt->id);
    } else {
        id = ltoh16_ua((const uint8 *)elt);
    }

    return id;
}

bcm_xtlv_t *bcm_next_xtlv(const bcm_xtlv_t *elt, int *buflen,
                          bcm_xtlv_opts_t opts)
{
    int sz;
    /* advance to next elt */
    sz = BCM_XTLV_SIZE_EX(elt, opts);
    elt = (const bcm_xtlv_t *)((const uint8 *)elt + sz);
    *buflen -= sz;

    /* validate next elt */
    if (!bcm_valid_xtlv(elt, *buflen, opts)) {
        return NULL;
    }

    GCC_DIAGNOSTIC_PUSH_SUPPRESS_CAST();
    return (bcm_xtlv_t *)(elt);
    GCC_DIAGNOSTIC_POP();
}

int bcm_xtlv_buf_init(bcm_xtlvbuf_t *tlv_buf, uint8 *buf, uint16 len,
                      bcm_xtlv_opts_t opts)
{
    if (!tlv_buf || !buf || !len) {
        return BCME_BADARG;
    }

    tlv_buf->opts = opts;
    tlv_buf->size = len;
    tlv_buf->head = buf;
    tlv_buf->buf = buf;
    return BCME_OK;
}

uint16 bcm_xtlv_buf_len(bcm_xtlvbuf_t *tbuf)
{
    uint16 len;

    if (tbuf) {
        len = (uint16)(tbuf->buf - tbuf->head);
    } else {
        len = 0;
    }

    return len;
}

uint16 bcm_xtlv_buf_rlen(bcm_xtlvbuf_t *tbuf)
{
    uint16 rlen;
    if (tbuf) {
        rlen = tbuf->size - bcm_xtlv_buf_len(tbuf);
    } else {
        rlen = 0;
    }

    return rlen;
}

uint8 *bcm_xtlv_buf(bcm_xtlvbuf_t *tbuf)
{
    return tbuf ? tbuf->buf : NULL;
}

uint8 *bcm_xtlv_head(bcm_xtlvbuf_t *tbuf)
{
    return tbuf ? tbuf->head : NULL;
}

void bcm_xtlv_pack_xtlv(bcm_xtlv_t *xtlv, uint16 type, uint16 len,
                        const uint8 *data, bcm_xtlv_opts_t opts)
{
    uint8 *data_buf;
    bcm_xtlv_opts_t mask = BCM_XTLV_OPTION_IDU8 | BCM_XTLV_OPTION_LENU8;

    if (!(opts & mask)) { /* default */
        uint8 *idp = (uint8 *)xtlv;
        uint8 *lenp = idp + sizeof(xtlv->id);
        htol16_ua_store(type, idp);
        htol16_ua_store(len, lenp);
        data_buf = lenp + sizeof(uint16);
    } else if ((opts & mask) == mask) { /* u8 id and u8 len */
        uint8 *idp = (uint8 *)xtlv;
        uint8 *lenp = idp + 1;
        *idp = (uint8)type;
        *lenp = (uint8)len;
        data_buf = lenp + sizeof(uint8);
    } else if (opts & BCM_XTLV_OPTION_IDU8) { /* u8 id, u16 len */
        uint8 *idp = (uint8 *)xtlv;
        uint8 *lenp = idp + 1;
        *idp = (uint8)type;
        htol16_ua_store(len, lenp);
        data_buf = lenp + sizeof(uint16);
    } else if (opts & BCM_XTLV_OPTION_LENU8) { /* u16 id, u8 len */
        uint8 *idp = (uint8 *)xtlv;
        uint8 *lenp = idp + sizeof(uint16);
        htol16_ua_store(type, idp);
        *lenp = (uint8)len;
        data_buf = lenp + sizeof(uint8);
    } else {
        bool Unexpected_xtlv_option = TRUE;
        BCM_REFERENCE(Unexpected_xtlv_option);
        ASSERT(!Unexpected_xtlv_option);
        return;
    }

    if (opts & BCM_XTLV_OPTION_LENU8) {
        ASSERT(len <= 0x00ff);
        len &= 0xff;
    }

    if (data != NULL) {
        memcpy(data_buf, data, len);
    }
}

/* xtlv header is always packed in LE order */
void bcm_xtlv_unpack_xtlv(const bcm_xtlv_t *xtlv, uint16 *type, uint16 *len,
                          const uint8 **data, bcm_xtlv_opts_t opts)
{
    if (type) {
        *type = (uint16)bcm_xtlv_id(xtlv, opts);
    }
    if (len) {
        *len = (uint16)bcm_xtlv_len(xtlv, opts);
    }
    if (data) {
        *data = (const uint8 *)xtlv + BCM_XTLV_HDR_SIZE_EX(opts);
    }
}

int bcm_xtlv_put_data(bcm_xtlvbuf_t *tbuf, uint16 type, const uint8 *data,
                      int n)
{
    bcm_xtlv_t *xtlv;
    int size;

    if (tbuf == NULL) {
        return BCME_BADARG;
    }

    size = bcm_xtlv_size_for_data(n, tbuf->opts);
    if (bcm_xtlv_buf_rlen(tbuf) < size) {
        return BCME_NOMEM;
    }

    xtlv = (bcm_xtlv_t *)bcm_xtlv_buf(tbuf);
    bcm_xtlv_pack_xtlv(xtlv, type, (uint16)n, data, tbuf->opts);
    tbuf->buf += size; /* note: data may be NULL, reserves space */
    return BCME_OK;
}

static int bcm_xtlv_put_int(bcm_xtlvbuf_t *tbuf, uint16 type, const uint8 *data,
                            int n, int int_sz)
{
    bcm_xtlv_t *xtlv;
    int xtlv_len;
    uint8 *xtlv_data;
    int err = BCME_OK;

    if (tbuf == NULL) {
        err = BCME_BADARG;
        goto done;
    }

    xtlv = (bcm_xtlv_t *)bcm_xtlv_buf(tbuf);

    /* put type and length in xtlv and reserve data space */
    xtlv_len = n * int_sz;
    err = bcm_xtlv_put_data(tbuf, type, NULL, xtlv_len);
    if (err != BCME_OK) {
        goto done;
    }

    xtlv_data = (uint8 *)xtlv + bcm_xtlv_hdr_size(tbuf->opts);

    /* write data w/ little-endianness into buffer - single loop, aligned access
     */
    for (; n != 0; --n, xtlv_data += int_sz, data += int_sz) {
        switch (int_sz) {
            case sizeof(uint8):
                break;
            case sizeof(uint16): {
                uint16 v = load16_ua(data);
                htol16_ua_store(v, xtlv_data);
                break;
            }
            case sizeof(uint32): {
                uint32 v = load32_ua(data);
                htol32_ua_store(v, xtlv_data);
                break;
            }
            case sizeof(uint64): {
                uint64 v = load64_ua(data);
                htol64_ua_store(v, xtlv_data);
                break;
            }
            default:
                err = BCME_UNSUPPORTED;
                goto done;
        }
    }

done:
    return err;
}

int bcm_xtlv_put16(bcm_xtlvbuf_t *tbuf, uint16 type, const uint16 *data, int n)
{
    return bcm_xtlv_put_int(tbuf, type, (const uint8 *)data, n, sizeof(uint16));
}

int bcm_xtlv_put32(bcm_xtlvbuf_t *tbuf, uint16 type, const uint32 *data, int n)
{
    return bcm_xtlv_put_int(tbuf, type, (const uint8 *)data, n, sizeof(uint32));
}

int bcm_xtlv_put64(bcm_xtlvbuf_t *tbuf, uint16 type, const uint64 *data, int n)
{
    return bcm_xtlv_put_int(tbuf, type, (const uint8 *)data, n, sizeof(uint64));
}

/*
 *  upacks xtlv record from buf checks the type
 *  copies data to callers buffer
 *  advances tlv pointer to next record
 *  caller's resposible for dst space check
 */
int bcm_unpack_xtlv_entry(const uint8 **tlv_buf, uint16 xpct_type,
                          uint16 xpct_len, uint8 *dst_data,
                          bcm_xtlv_opts_t opts)
{
    const bcm_xtlv_t *ptlv = (const bcm_xtlv_t *)*tlv_buf;
    uint16 len;
    uint16 type;
    const uint8 *data;

    ASSERT(ptlv);

    bcm_xtlv_unpack_xtlv(ptlv, &type, &len, &data, opts);
    if (len) {
        if ((type != xpct_type) || (len > xpct_len)) {
            return BCME_BADARG;
        }
        if (dst_data && data) {
            memcpy(dst_data, data, len); /* copy data to dst */
        }
    }

    *tlv_buf += BCM_XTLV_SIZE_EX(ptlv, opts);
    return BCME_OK;
}

/*
 *  packs user data into tlv record and advances tlv pointer to next xtlv slot
 *  buflen is used for tlv_buf space check
 */
int bcm_pack_xtlv_entry(uint8 **tlv_buf, uint16 *buflen, uint16 type,
                        uint16 len, const uint8 *src_data, bcm_xtlv_opts_t opts)
{
    bcm_xtlv_t *ptlv = (bcm_xtlv_t *)*tlv_buf;
    int size;

    ASSERT(ptlv);

    size = bcm_xtlv_size_for_data(len, opts);
    /* copy data from tlv buffer to dst provided by user */
    if (size > *buflen) {
        return BCME_BADLEN;
    }

    bcm_xtlv_pack_xtlv(ptlv, type, len, src_data, opts);

    /* advance callers pointer to tlv buff */
    *tlv_buf = (uint8 *)(*tlv_buf) + size;
    /* decrement the len */
    *buflen -= (uint16)size;
    return BCME_OK;
}

/*
 *  unpack all xtlv records from the issue a callback
 *  to set function one call per found tlv record
 */
int bcm_unpack_xtlv_buf(void *ctx, const uint8 *tlv_buf, uint16 buflen,
                        bcm_xtlv_opts_t opts, bcm_xtlv_unpack_cbfn_t *cbfn)
{
    uint16 len;
    uint16 type;
    int res = BCME_OK;
    int size;
    const bcm_xtlv_t *ptlv;
    int sbuflen = buflen;
    const uint8 *data;
    int hdr_size;

    ASSERT(!buflen || tlv_buf);
    ASSERT(!buflen || cbfn);

    hdr_size = BCM_XTLV_HDR_SIZE_EX(opts);
    while (sbuflen >= hdr_size) {
        ptlv = (const bcm_xtlv_t *)tlv_buf;

        bcm_xtlv_unpack_xtlv(ptlv, &type, &len, &data, opts);
        size = bcm_xtlv_size_for_data(len, opts);

        sbuflen -= size;
        if (sbuflen < 0) { /* check for buffer overrun */
            break;
        }

        if ((res = cbfn(ctx, data, type, len)) != BCME_OK) {
            break;
        }
        tlv_buf += size;
    }
    return res;
}

int bcm_pack_xtlv_buf(void *ctx, uint8 *tlv_buf, uint16 buflen,
                      bcm_xtlv_opts_t opts,
                      bcm_pack_xtlv_next_info_cbfn_t get_next,
                      bcm_pack_xtlv_pack_next_cbfn_t pack_next, int *outlen)
{
    int res = BCME_OK;
    uint16 tlv_id;
    uint16 tlv_len;
    uint8 *startp;
    uint8 *endp;
    uint8 *buf;
    bool more;
    int size;
    int hdr_size;

    ASSERT(get_next && pack_next);

    buf = tlv_buf;
    startp = buf;
    endp = (uint8 *)buf + buflen;
    more = TRUE;
    hdr_size = BCM_XTLV_HDR_SIZE_EX(opts);

    while (more && (buf < endp)) {
        more = get_next(ctx, &tlv_id, &tlv_len);
        size = bcm_xtlv_size_for_data(tlv_len, opts);
        if ((buf + size) > endp) {
            res = BCME_BUFTOOSHORT;
            goto done;
        }

        bcm_xtlv_pack_xtlv((bcm_xtlv_t *)buf, tlv_id, tlv_len, NULL, opts);
        pack_next(ctx, tlv_id, tlv_len, buf + hdr_size);
        buf += size;
    }

    if (more) {
        res = BCME_BUFTOOSHORT;
    }

done:
    if (outlen) {
        *outlen = (int)(buf - startp);
    }
    return res;
}

/*
 *  pack xtlv buffer from memory according to xtlv_desc_t
 */
int bcm_pack_xtlv_buf_from_mem(uint8 **tlv_buf, uint16 *buflen,
                               const xtlv_desc_t *items, bcm_xtlv_opts_t opts)
{
    int res = BCME_OK;
    uint8 *ptlv = *tlv_buf;

    while (items->type != 0) {
        if (items->len && items->ptr) {
            res = bcm_pack_xtlv_entry(&ptlv, buflen, items->type, items->len,
                                      items->ptr, opts);
            if (res != BCME_OK) {
                break;
            }
        }
        items++;
    }

    *tlv_buf = ptlv; /* update the external pointer */
    return res;
}

/*
 *  unpack xtlv buffer to memory according to xtlv_desc_t
 *
 */
int bcm_unpack_xtlv_buf_to_mem(uint8 *tlv_buf, int *buflen, xtlv_desc_t *items,
                               bcm_xtlv_opts_t opts)
{
    int res = BCME_OK;
    bcm_xtlv_t *elt;

    elt = bcm_valid_xtlv((bcm_xtlv_t *)tlv_buf, *buflen, opts)
              ? (bcm_xtlv_t *)tlv_buf
              : NULL;
    if (!elt || !items) {
        res = BCME_BADARG;
        return res;
    }

    for (; elt != NULL && res == BCME_OK;
         elt = bcm_next_xtlv(elt, buflen, opts)) {
        /*  find matches in desc_t items  */
        xtlv_desc_t *dst_desc = items;
        uint16 len, type;
        const uint8 *data;

        bcm_xtlv_unpack_xtlv(elt, &type, &len, &data, opts);
        while (dst_desc->type != 0) {
            if (type == dst_desc->type) {
                if (len != dst_desc->len) {
                    res = BCME_BADLEN;
                } else {
                    memcpy(dst_desc->ptr, data, len);
                }
                break;
            }
            dst_desc++;
        }
    }

    if (res == BCME_OK && *buflen != 0) {
        res = BCME_BUFTOOSHORT;
    }

    return res;
}

/*
 * return data pointer of a given ID from xtlv buffer.
 * If the specified xTLV ID is found, on return *datalen will contain
 * the the data length of the xTLV ID.
 */
const uint8 *bcm_get_data_from_xtlv_buf(const uint8 *tlv_buf, uint16 buflen,
                                        uint16 id, uint16 *datalen,
                                        bcm_xtlv_opts_t opts)
{
    const uint8 *retptr = NULL;
    uint16 type, len;
    int size;
    const bcm_xtlv_t *ptlv;
    int sbuflen = buflen;
    const uint8 *data;
    int hdr_size;

    hdr_size = BCM_XTLV_HDR_SIZE_EX(opts);

    /* Init the datalength */
    if (datalen) {
        *datalen = 0;
    }
    while (sbuflen >= hdr_size) {
        ptlv = (const bcm_xtlv_t *)tlv_buf;
        bcm_xtlv_unpack_xtlv(ptlv, &type, &len, &data, opts);

        size = bcm_xtlv_size_for_data(len, opts);
        sbuflen -= size;
        if (sbuflen < 0) { /* buffer overrun? */
            break;
        }

        if (id == type) {
            retptr = data;
            if (datalen) {
                *datalen = len;
            }
            break;
        }

        tlv_buf += size;
    }

    return retptr;
}

bcm_xtlv_t *bcm_xtlv_bcopy(const bcm_xtlv_t *src, bcm_xtlv_t *dst,
                           int src_buf_len, int dst_buf_len,
                           bcm_xtlv_opts_t opts)
{
    bcm_xtlv_t *dst_next = NULL;
    src = (src && bcm_valid_xtlv(src, src_buf_len, opts)) ? src : NULL;
    if (src && dst) {
        uint16 type;
        uint16 len;
        const uint8 *data;
        int size;
        bcm_xtlv_unpack_xtlv(src, &type, &len, &data, opts);
        size = bcm_xtlv_size_for_data(len, opts);
        if (size <= dst_buf_len) {
            bcm_xtlv_pack_xtlv(dst, type, len, data, opts);
            dst_next = (bcm_xtlv_t *)((uint8 *)dst + size);
        }
    }

    return dst_next;
}
